/*

Internet Central Heating Timer.

Connect up the CA-42 serial lead as usual:

    Lead Pin 1 to ground,
    Lead Pin 2 (Rx) to AVR Pin 3 (PD1 - Tx)
    Lead Pin 3 (Tx) to AVR Pin 2 (PD0 - Rx)

Connect a 10k resistor between AVR pin 11 (arduino digital pin 5) and ground, and a
push-button switch between that pin and +5V.

Wire up a 5.5v regulator

Wire up a 3.3v regulator (see Datasheets\LD33CV 3.3v regulator.txt)

Wire up a 74HC4050 chip - so that we downstep the serial signal going into the wifly - with
the following connections:

    Pin 1: +3.3V
    Pin 2: (goes to wifly pin 3 - see below)
    Pin 3: ATMega pin 5 (arduino pin 3 outputting 5V - see WIFLY_TX_PIN below)
    Pin 8: GND

Connect up the following WiFly pins (pin 1 is top left when you're looking at 
it from above with the antenna showing):

    Pin 1:  (VCC)   +3.3V
    Pin 2:  (TX)    ATMega pin 6 (arduino pin 4 - see WIFLY_RX_PIN below)
    Pin 3:  (RX)    74HC4050 pin 2
    Pin 8:  (GPIO9) GND via a 1k resistor, and +5V via a toggle switch
    Pin 10: (GND)   GND

The WiFly module needs to be configured (via telnet on 192.168.50.105 port 2000) as follows:

    set wlan ssid xxxx          (where xxxx is the full SSID, with spaces replaced with $ signs)
    set wlan passphrase xxxx
    set sys auto 0
    set option format 1
    set dns name avr.musicbysines.com
    set ip host 0               (forces it to use DNS)
    set ip remote 80
    set ip proto 18
    set com remote GET$/heating/readonly.php?DATA=     (when you change this, remember to keep the "?DATA=" bit!)        
    set uart mode 2             (i.e. send HTTP request upon receiving UART data)
    set comm time 30            (i.e. how long to wait after opening connection on first UART char)
    set time zone 0
    set time enable 0           (i.e. how long to wait between trips to the NPT time server)
    set comm size 120           (i.e. flush size - allow more request bytes to be gathered over uart)
    set comm time 100           (i.e. flush time - allow longer delay before deciding that we're not going to get any more request bytes over uart)
    save
    reboot
    
When it's working, the 'get everything' command returns these values:

    WiFly Ver 2.28, 09-08-2011 on RN-171
    Beacon=100
    Probe=5
    OPEN=*OPEN*
    CLOSE=*CLOS*
    REMOTE=GET /avr/readonly.php?DATA=
    FlushSize=75
    MatchChar=0
    FlushTimer=50
    IdleTimer=0
    CmdChar=$
    IF=UP
    DHCP=ON
    IP=192.168.50.105:2000
    NM=255.255.255.0
    GW=192.168.50.1
    HOST=173.236.134.24:80
    PROTO=TCP,HTTP,
    MTU=1524
    FLAGS=0x7
    TCPMODE=0x0
    BACKUP=0.0.0.0
    DNS=194.168.4.100
    Name=www.mossley.org
    Backup=backup2
    FTP=208.109.78.34:21
    File=wifly-EZX.img
    User=roving
    Pass=Pass123
    Dir=public
    Timeout=40
    FTP_mode=0x0
    SSID=Michael and Kelly
    Chan=0
    ExtAnt=0
    Join=1
    Auth=OPEN
    Mask=0x1fff
    Rate=12, 24 Mb
    Linkmon=0
    Passphrase=xxx
    TxPower=0
    SleepTmr=0
    WakeTmr=0
    Trigger=0x1
    Autoconn=0
    IoFunc=0x0
    IoMask=0x21f0
    IoValu=0x0
    PrintLvl=0x1
    TimeEna=0
    TIMEADR=129.6.15.28:123
    Zone=7
    Baudrate=9600
    Flow=0x0
    Mode=0x2
    JoinTmr=1000
    Replace=0x24
    DeviceId=WiFly-EZX
    Password=
    Format=0x25
    Signal=0
    Average=5
    BCAST=255.255.255.255:55555
    Interval=0x7
    Sensor=0x0
    SensePwr=0x0

You can put the WiFly into ADHOC mode by closing the switch on GPIO9 and reconnecting power.
You'll know it's in ADHOC mode when the red LED on the WiFly blinks all the time.
You can then connect to its wireless network from your laptop, telnet to 169.254.1.1 on port 2000
where you can issue the $$$ command and use e.g.
    set wlan ssid Michael$and$Kelly
    set wlan passphrase xxxxxxxx


For the LCD display:
    Use a 1k potentiometer with the middle connected to LCD Vo and the others connected to +ve supply and ground.
    Use a 330 resistor between LED- and ground.
    Attach the others to +ve supply or ground as appropriate.

    I've used the following pins on the ATmega328 to drive the data pins on the LCD:

    LCD     MCU
    -----   ---
    RS      11
    R/w     Tied to ground
    E       12

    Data4   13
    Data5   A0
    Data6   A1
    Data7   A2
    
    
    
    
HOSTING
=======    

avr.musicbysines.com
mysql.avr.musicbysines.com
DB name: avrdb
User name: avr
Password: c*

*/


#include <WProgram.h>
#include <NewSoftSerial.h>
#include <LiquidCrystal.h>
#include <Time.h>
#include <avr/pgmspace.h>
#include <Encoder.h>
#include "HeatingWifly.h"

//
// Simple macro to copy the given string into a useable buffer called vsProgmemBuffer_m.
// example use here:
//
//      GET_PROGMEM_STRING(THREE);
//      realSerial.println(vsProgmemBuffer_m);
//
#define GET_PROGMEM_STRING(sProgCharString) strcpy_P(vsProgmemBuffer_m, sProgCharString)



const int CR = 13;
const int LF = 10;

// Analog pins
const int A0 = 14;
const int A1 = 15;
const int A2 = 16;
const int A3 = 17;
const int A4 = 18;
const int A5 = 19;




// Input/Output pins
const int ENCODER_WHEEL_PIN_1 = 4;
const int ENCODER_WHEEL_PIN_2 = A4;
const int ENCODER_BUTTON_PIN = 5;
//const int BACK_BUTTON_PIN = 99;
const int ADVANCE_BUTTON_PIN = 6;
const int SERIAL_RX_PIN = 3;
const int SERIAL_TX_PIN = 2;
const int WATER_RELAY_PIN = 7; 
const int HEATING_RELAY_PIN = 8;
const int TEMPERATURE_SENSOR_PIN = A5;
const int LCD_CONTROL_PIN_1 = 11;
const int LCD_CONTROL_PIN_2 = 12;
const int LCD_DATA_PIN_1 = 13;
const int LCD_DATA_PIN_2 = A0;
const int LCD_DATA_PIN_3 = A1;
const int LCD_DATA_PIN_4 = A2;
const int GPIO_PINS[2] = { 9, 10 };
const int NUM_GPIO_PINS = 2;


// HTTP Parsing states
const int PARSE_STATE_IDLE = 0;
const int PARSE_STATE_FOUND_ASTERISK = 1;
const int PARSE_STATE_FOUND_OPEN = 2;
const int PARSE_STATE_FOUND_NEWLINE = 3;
const int PARSE_STATE_READING_MESSAGE_BODY = 4;



const int MAX_DAY_NAME_LENGTH = 3;
const int MAX_TIME_LENGTH = 5;
const int MAX_MOTD_LENGTH = 16;
const int MAX_HTTP_BODY_BYTES = 69;

const int APPLIANCES_NONE = 0;
const int APPLIANCES_HOT_WATER_ONLY = 1;
const int APPLIANCES_HOT_WATER_AND_CENTRAL_HEATING = 2;
const int APPLIANCES_MAX = 2;

const int ADVANCE_AS_SCHEDULED = 0;
const int ADVANCE_PLUS_ONE_HOUR = 1;
const int ADVANCED_TO_NEXT_EVENT = 2;
const char RIGHT_ARROW_SYMBOL = 126;

const int DAYS_IN_A_WEEK = 7;
const int WINDOWS_PER_DAY = 3;
const int MINUTES_IN_A_DAY = 1440;
const unsigned long MILLIS_IN_AN_HOUR = 10000UL;// was 3600000UL;



const int UI_STATE_SYNCHRONISING = 97;
const int UI_STATE_SYNCHRONISATION_COMPLETE = 98;
const int UI_STATE_SYNCHRONISATION_FAILED = 99;
const int UI_STATE_MENU_MIN = 0;
const int UI_STATE_NORMAL = 0;
const int UI_STATE_MENU_1_MAX_TEMPERATURE = 1;
const int UI_STATE_MENU_2_APPLIANCES = 2;
const int UI_STATE_MENU_3_WINDOW_TIMES_START = 3;
const int UI_STATE_MENU_3_WINDOW_TIMES_END = 23;
const int UI_STATE_MENU_4_CLOCK = 24;
const int UI_STATE_MENU_5_DISPLAY_LAST_SYNC_TIME = 25;
const int UI_STATE_MENU_6_TOGGLE_SYNC = 26;
const int UI_STATE_MENU_7_SYNC_DELAY = 27;
const int UI_STATE_MENU_MAX = 27;
const int UI_STATE_EDIT_CLOCK_1_DAY_OF_WEEK = 40;
const int UI_STATE_EDIT_CLOCK_2_HOUR = 41;
const int UI_STATE_EDIT_CLOCK_3_MINUTES = 42;
const int UI_STATE_EDIT_MAX_TEMPERATURE = 43;
const int UI_STATE_EDIT_APPLIANCES = 44;
const int UI_STATE_EDIT_SYNC_DELAY = 45;
const int UI_STATE_EDIT_WINDOW_TIMES_START = 50;
const int UI_STATE_EDIT_WINDOW_TIMES_END = 92;
const int UI_STATE_MAX = 92;

const unsigned long MAX_UI_STATE_IDLE_TIME_MILLIS = 120000;
const unsigned long DEFAULT_INTERNET_SYNC_DELAY_MILLIS = 10000; // 2 mins
const unsigned long HTTP_TIMEOUT = 3000;
const char* HTTP_BODY_SEARCH_STRING = "\r\n\r\nOK";
const int MAX_HTTP_BODY_CHARS_TO_READ = 73;

const char DEGREES_SYMBOL = 223;
const byte MAX_TEMPERATURE_SAMPLES = 3;
const unsigned long TEMPERATURE_SAMPLE_DELAY_MILLIS = 60000;

const char LONG_DAY_NAMES[DAYS_IN_A_WEEK][4] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
const char SHORT_DAY_NAMES[DAYS_IN_A_WEEK][3] = { "Su", "M ", "Tu", "W ", "Th", "F ", "Sa" };
 

const char* BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

PROGMEM prog_char MSG_LCD_SYNCHRONISING[] = "Synchronising...";
PROGMEM prog_char MSG_LCD_SYNCHRONISED[] = "Yay!            ";
PROGMEM prog_char MSG_LCD_TIMED_OUT[] = "Timed out       ";
PROGMEM prog_char MSG_LCD_SET_MAX_TEMP_QUESTION[] = "Set max temp?";
PROGMEM prog_char MSG_LCD_CURRENT_TEMP[] = "Current:  ";
PROGMEM prog_char MSG_LCD_SET_MODE_QUESTION[] = "Set mode?";
PROGMEM prog_char MSG_LCD_CURRENT_MODE[] = "Current:  ";
PROGMEM prog_char MSG_LCD_MODE_OFF[] = "Off";
PROGMEM prog_char MSG_LCD_MODE_HW[] = "HW";
PROGMEM prog_char MSG_LCD_MODE_HW_CH[] = "HW&CH";
PROGMEM prog_char MSG_LCD_SET_WINDOW_QUESTION_1[] = "Set ";
PROGMEM prog_char MSG_LCD_SET_WINDOW_QUESTION_2[] = " times?";
PROGMEM prog_char MSG_LCD_SET_WINDOW_TO[] = " to ";
PROGMEM prog_char MSG_LCD_LAST_SYNC_WAS[] = "Last sync was";
PROGMEM prog_char MSG_LCD_LAST_SYNC_NEVER[] = "never!";
PROGMEM prog_char MSG_LCD_LAST_SYNC_AGES_AGO[] = "ages ago";
PROGMEM prog_char MSG_LCD_LAST_SYNC_MINUTES_AGO[] = " mins ago";
PROGMEM prog_char MSG_LCD_INTERNET_SYNC[] = "Internet sync";
PROGMEM prog_char MSG_LCD_INTERNET_SYNC_ENABLED_QUESTION[] = "enabled? ";
PROGMEM prog_char MSG_LCD_INTERNET_SYNC_ENABLED_YES[] = "YES";
PROGMEM prog_char MSG_LCD_INTERNET_SYNC_ENABLED_NO[] = "NO";
PROGMEM prog_char MSG_LCD_SET_SYNC_DELAY_QUESTION[] = "Set sync delay?";
PROGMEM prog_char MSG_LCD_SET_SYNC_DELAY_CURRENT[] = "Current: ";
PROGMEM prog_char MSG_LCD_SET_CLOCK[] = "Set clock"     ;
PROGMEM prog_char MSG_LCD_SET_MAX_TEMP[] = "Set max temp";
PROGMEM prog_char MSG_LCD_SET_MAX_TEMP_NEW[] = "New:     [";
PROGMEM prog_char MSG_LCD_SET_MODE[] = "Set mode";
PROGMEM prog_char MSG_LCD_SET_MODE_NEW[] = "New:     ["        ;
PROGMEM prog_char MSG_LCD_SET_MODE_OFF[] = "Off  "                ;
PROGMEM prog_char MSG_LCD_SET_MODE_HW[] = "HW   ";
PROGMEM prog_char MSG_LCD_SET_MODE_BOTH[] = "HW&CH";
PROGMEM prog_char MSG_LCD_SET_SYNC_DELAY[] = "Set sync delay";
PROGMEM prog_char MSG_LCD_MODE_SHORT_OFF[] = "off";
PROGMEM prog_char MSG_LCD_MODE_SHORT_HW[] = " W "   ;
PROGMEM prog_char MSG_LCD_MODE_SHORT_BOTH[] = "W&H";
PROGMEM prog_char MSG_LCD_SET_SYNC_DELAY_NEW[] = "New:    [";
PROGMEM prog_char MSG_LCD_SET_WINDOW_TIMES_1[] = "Set ";
PROGMEM prog_char MSG_LCD_SET_WINDOW_TIMES_2[] = " times";
PROGMEM prog_char MSG_LCD_UNKNOWN_UI_STATE[] = "Unknown UI state";
PROGMEM prog_char MSG_LCD_HELLO[] = "Hello";
PROGMEM prog_char MSG_SERIALDEBUG_INIT[] = "Serial init.."        ;
PROGMEM prog_char MSG_SERIALDEBUG_PARSING_HEATING_INFO[] = "Parsing";
PROGMEM prog_char MSG_SERIALDEBUG_GPIO_SETTING[] = "GPIO";
PROGMEM prog_char MSG_SERIALDEBUG_DAY_WINDOW_SETTING[] = "Day";
PROGMEM prog_char MSG_SERIALDEBUG_SENDING_1[] = "Sending [";
PROGMEM prog_char MSG_SERIALDEBUG_SENDING_2[] = "], b64 [";
PROGMEM prog_char MSG_SERIALDEBUG_SENDING_3[] = "]";
PROGMEM prog_char MSG_SERIALDEBUG_SENDING_RESULT[] = "Result ";
PROGMEM prog_char MSG_WIFLY_STARTUP_MESSAGE[] = "STARTUP";




struct HeatingInfo
{
    byte currentDay;
    int ambientTemp;
    bool isAmbientTempSet;
    byte maxTemp; 
    byte currentTimeHours;
    byte currentTimeMins; 
    char messageOfTheDay[MAX_MOTD_LENGTH+1];
    byte appliances;
    byte advanceStatus;
    unsigned long lastAdvanceStatusChangeMillis;
    bool internetSyncEnabled;
    char startWindows[DAYS_IN_A_WEEK][WINDOWS_PER_DAY];  // Stores times as ints e.g. 00:00 = 0 and 23:50 = 235
    char endWindows[DAYS_IN_A_WEEK][WINDOWS_PER_DAY];  // Stores times as ints e.g. 00:00 = 0 and 23:50 = 235
    bool wasInWindow;
    bool gpioStatuses[NUM_GPIO_PINS];
};


HeatingWifly wifly_m;
NewSoftSerial realSerial_m(SERIAL_RX_PIN, SERIAL_TX_PIN, false);  
int iState_m = PARSE_STATE_IDLE;
char vsSendBuffer_m[50];
const int MAX_BASE64_SEND_BUFFER_SIZE = 70;
char vsBase64SendBuffer_m[MAX_BASE64_SEND_BUFFER_SIZE];  


const int MAX_PROGMEM_BUFFER_SIZE = 20;
char vsProgmemBuffer_m[MAX_PROGMEM_BUFFER_SIZE];

HeatingInfo heatingInfo_m;
bool isHeatingInfoChangedLocally_m = false;
unsigned long lLastSynchroniseSendTime_m = 0;
unsigned long lLastSynchroniseSuccessTime_m = 0;
unsigned long lLastTemperatureSampleTime_m = 0;
unsigned long lLastUIStateChangeTime_m = 0;
int temperatureSamples_m[MAX_TEMPERATURE_SAMPLES] = { 0, 0, 0};
unsigned long lLastTimeCheckTime_m = 0;
unsigned long lMaxInternetSyncDelayMillis_m = DEFAULT_INTERNET_SYNC_DELAY_MILLIS;


byte yUIState_m = UI_STATE_NORMAL;
byte yPreviousUIState_m = UI_STATE_NORMAL;

int iEncoderValue_m = 0;
Encoder encoder_m(ENCODER_WHEEL_PIN_1, ENCODER_WHEEL_PIN_2);
LiquidCrystal lcd_m(LCD_CONTROL_PIN_1, LCD_CONTROL_PIN_2, LCD_DATA_PIN_1, LCD_DATA_PIN_2, LCD_DATA_PIN_3, LCD_DATA_PIN_4);


void updateDisplay(bool isPartial_p);

void parseHeatingInfo();
void readAllAvailableCharsOnWifly();
bool checkForWiflyOutput();
void synchroniseWithInternet(bool bForceSynchronise_p);





int base64Encode(char* psOriginal_p, char* psOutput_p, int iMaxOutputChars_p)
{
    
    bool isEndReached = false;
    int iInputCharsProcessed = 0;
    int iOutputCharsProcessed = 0;
    int iNumBlankBytes = 0;
    
    //
    //  We're dealing in blocks of 3 chars - i.e. 24 bits.  This allows us to deal
    //  with 4 groups of 6 bits.
    //
    while (! isEndReached)
    {
        char c1 = psOriginal_p[iInputCharsProcessed++];
        char c2 = psOriginal_p[iInputCharsProcessed++];
        char c3 = psOriginal_p[iInputCharsProcessed++];
        
        // If any of these are NULLs then we will be finishing soon.        
        if (0 == c1)
        {
            // c1, c2, and c3 are of no consequence so just return now.
            isEndReached = true;            
            break;
        }
        else if (0 == c2)
        {
            // We can process c1, but leave c2 and c3 as NULL.
            isEndReached = true;
            iNumBlankBytes = 2;
            c3 = 0;
        }
        else if (0 == c3)        
        {
            // We can process c1 and c2, but leave c3 as NULL.
            isEndReached = true;
            iNumBlankBytes = 1;
        }
            
        byte b1 = ((c1 & 0xFC) >> 2);                      // First 6 bits of c1
        byte b2 = ((c1 & 0x03) << 4) + ((c2 & 0xF0) >> 4); // Last 2 bits of c1 and first 4 bits of c2
        byte b3 = ((c2 & 0x0F) << 2) + ((c3 & 0xC0) >> 6); // Last 4 bits of c2 and first 2 bits of c3
        byte b4 =  (c3 & 0x3F);                            // Last 6 bits of c3
        
        // Now convert these values into their base64 chars and add them to the output string
        if (iOutputCharsProcessed + 4 > iMaxOutputChars_p)
        {
            return -1;
        }
        
        psOutput_p[iOutputCharsProcessed++] = BASE64_CHARS[b1];
        psOutput_p[iOutputCharsProcessed++] = BASE64_CHARS[b2];
        psOutput_p[iOutputCharsProcessed++] = BASE64_CHARS[b3];
        psOutput_p[iOutputCharsProcessed++] = BASE64_CHARS[b4];
        
        while (iNumBlankBytes > 0)
        {
            psOutput_p[iOutputCharsProcessed - iNumBlankBytes] = '=';
            iNumBlankBytes--;
        }
    }
    
    return iOutputCharsProcessed;   
}

/*

void testBase64Encoding(char* psTestName_p, char* psAscii_p, char* psExpectedBase64_p)
{
    char szActualBase64[100];
    int iOutputChars = base64Encode(psAscii_p, szActualBase64, 100);
    
    if (-1 == iOutputChars)
    {        
        Serial.print("Test failed - ");
        Serial.print(psTestName_p);
        Serial.print(" - overflow in the output buffer");
        Serial.println();
    }
    else if (0 != strcmp(psExpectedBase64_p, szActualBase64))
    {
        Serial.print("Test failed - ");
        Serial.print(psTestName_p);
        Serial.print(" - expected '");
        Serial.print(psExpectedBase64_p);
        Serial.print("' but got '");
        Serial.print(szActualBase64);
        Serial.print("'");
        Serial.println();
    }
}

*/



const char* getLongDayName(byte yDayIndex_p)
{
    return LONG_DAY_NAMES[yDayIndex_p];
}

const char* getShortDayName(byte yDayIndex_p)
{
    return SHORT_DAY_NAMES[yDayIndex_p];
}
                    
                    
unsigned long millisSince(unsigned long lLastMillis_p)
{
    unsigned long lCurrentMillis = millis();
    
    if (lCurrentMillis < lLastMillis_p)
    {
        // Assume rollover
        return lCurrentMillis + (0xFFFFFFFFUL - lLastMillis_p);
    }
    else
    {
        return lCurrentMillis - lLastMillis_p;   
    }   
}                    



void waitForDebounce(int iPin_p)
{
    int iOriginalState = digitalRead(iPin_p);
    
    while (iOriginalState == digitalRead(iPin_p))
    {
        delay(100);
    }
    delay(100);
}




bool isWithinWindow(char cStart_p, char cEnd_p)
{
    char cCurrentTime = (heatingInfo_m.currentTimeHours * 10) + (heatingInfo_m.currentTimeMins / 10);
    
    return cCurrentTime >= cStart_p  &&  cCurrentTime < cEnd_p;
}


void updateRelays()
{    
    // Flash LEDs to show we mean business
    for (int i=0; i < 0; i++)
    {
        digitalWrite(WATER_RELAY_PIN, HIGH);
        digitalWrite(HEATING_RELAY_PIN, HIGH);
        delay(200);
        digitalWrite(WATER_RELAY_PIN, LOW);
        digitalWrite(HEATING_RELAY_PIN, LOW);
        delay(200);
    }
    
    for (byte yGPIO = 0; yGPIO < NUM_GPIO_PINS; yGPIO++)
    {
        digitalWrite(GPIO_PINS[yGPIO], heatingInfo_m.gpioStatuses[yGPIO] ? HIGH : LOW);
    }
    
    // TODO - look at the current time, figure out if one or both
    // relays should be on/off, and set the output pins appropriately.   
    byte yDay = heatingInfo_m.currentDay;
    bool isNowInWindow = false;
    
    for (byte yWindow=0; yWindow < WINDOWS_PER_DAY; yWindow++)
    {
        if (isWithinWindow(heatingInfo_m.startWindows[yDay][yWindow], heatingInfo_m.endWindows[yDay][yWindow]))
        {
            isNowInWindow = true;
            break;
        }
    }
    
    /*
        Previous state      New state       Advance state  Reset Advance?   Action
        -------------       ---------       -------------  --------------   ------
        Not in window       Not in window   None                            Relays off
        Not in window       Not in window   +1 hour        If > 1hr         If within 1 hour of when you clicked the button then Relays on, else set advance to 'none'
        Not in window       Not in window   Advance                         Relays on
        Not in window       In window       None                            Relays on
        Not in window       In window       +1 hour        Yes              Relays on, and set advance to 'none'
        Not in window       In window       Advance        Yes              Relays on, and set advance to 'none'
        In window           Not in window   None                            Relays off
        In window           Not in window   +1 hour        Yes              Relays off, and set advance to 'none'
        In window           Not in window   Advance        Yes              Relays off, and set advance to 'none'
        In window           In window       None                            Relays on
        In window           In window       +1 hour        If > 1hr         If within 1 hour of when you clicked the button then Relays off, else set advance to 'none'
        In window           In window       Advance                         Relays off
        
    Once we've sorted out all of the resetting of 'advance' status, it boils down to:
        New state       Advance state   Relays
        ---------       -------------   ------
        Not in window   None            Off
        Not in window   +1/Advance      On
        In window       None            On
        In window       +1/Advance      Off
        
    */
    // Reset the advance status if we've crossed a window boundary
    bool isAdvanceStatusChanged = false;
    
    if (heatingInfo_m.wasInWindow != isNowInWindow)
    {
        if (ADVANCE_AS_SCHEDULED != heatingInfo_m.advanceStatus)
        {
            isAdvanceStatusChanged = true;
        }
        
        heatingInfo_m.advanceStatus = ADVANCE_AS_SCHEDULED;        
    }                                      
    heatingInfo_m.wasInWindow = isNowInWindow;
    
    if (ADVANCE_PLUS_ONE_HOUR == heatingInfo_m.advanceStatus  &&  millisSince(heatingInfo_m.lastAdvanceStatusChangeMillis) > MILLIS_IN_AN_HOUR)
    {
        if (ADVANCE_AS_SCHEDULED != heatingInfo_m.advanceStatus)
        {
            isAdvanceStatusChanged = true;
        }
        
        heatingInfo_m.advanceStatus = ADVANCE_AS_SCHEDULED;
    }
    
    bool isWindowOverridden = (ADVANCE_AS_SCHEDULED != heatingInfo_m.advanceStatus);    
    bool bActivateRelays = (isNowInWindow != isWindowOverridden);  // XOR
    
    if (APPLIANCES_HOT_WATER_ONLY == heatingInfo_m.appliances)
    {
        digitalWrite(HEATING_RELAY_PIN, LOW);
        digitalWrite(WATER_RELAY_PIN, (bActivateRelays ? HIGH : LOW));
    }
    else if (APPLIANCES_HOT_WATER_AND_CENTRAL_HEATING == heatingInfo_m.appliances)
    {
        bool isRoomTooCold = (heatingInfo_m.ambientTemp < heatingInfo_m.maxTemp);
        
        digitalWrite(HEATING_RELAY_PIN, (bActivateRelays && isRoomTooCold) ? HIGH : LOW);
        digitalWrite(WATER_RELAY_PIN, (bActivateRelays ? HIGH : LOW));
    }
    else
    {
        digitalWrite(HEATING_RELAY_PIN, LOW);
        digitalWrite(WATER_RELAY_PIN, LOW);
    }
    
    if (isAdvanceStatusChanged)
    {
        updateDisplay(false);
    }
}





void setUIState(byte yNewUIState_p, bool isPartialDisplayUpdate_p)
{
    lLastUIStateChangeTime_m = millis();
    yUIState_m = yNewUIState_p;
    
    updateDisplay(isPartialDisplayUpdate_p);
    
    if (UI_STATE_NORMAL == yNewUIState_p)
    {
        updateRelays();
    }
}



void updateDisplay(bool isPartial_p)
{
    lcd_m.clear();
    lcd_m.setCursor(0, 0);
    
    if (UI_STATE_NORMAL == yUIState_m  ||  UI_STATE_SYNCHRONISATION_FAILED == yUIState_m  ||
        UI_STATE_SYNCHRONISATION_COMPLETE == yUIState_m  ||  UI_STATE_SYNCHRONISING == yUIState_m)
    {
        lcd_m.print(getShortDayName(heatingInfo_m.currentDay));   
        
        if (heatingInfo_m.currentTimeHours < 10)
        {
            lcd_m.print("0");
        }
        lcd_m.print(heatingInfo_m.currentTimeHours, DEC);
        lcd_m.print(":");
        if (heatingInfo_m.currentTimeMins < 10)
        {
            lcd_m.print("0");
        }
        lcd_m.print(heatingInfo_m.currentTimeMins, DEC);
        lcd_m.print(" ");
        
        lcd_m.print(heatingInfo_m.ambientTemp, DEC);
        lcd_m.print(DEGREES_SYMBOL);
        lcd_m.print(" ");
        
        if (APPLIANCES_HOT_WATER_AND_CENTRAL_HEATING == heatingInfo_m.appliances)
        {
            GET_PROGMEM_STRING(MSG_LCD_MODE_SHORT_BOTH);  // lcd_m.print("W&H");
            lcd_m.print(vsProgmemBuffer_m);
        }
        else if (APPLIANCES_HOT_WATER_ONLY == heatingInfo_m.appliances)
        {
            GET_PROGMEM_STRING(MSG_LCD_MODE_SHORT_HW);  // lcd_m.print(" W ");   
            lcd_m.print(vsProgmemBuffer_m);
        }
        else
        {
            GET_PROGMEM_STRING(MSG_LCD_MODE_SHORT_OFF);  // lcd_m.print("off");
            lcd_m.print(vsProgmemBuffer_m);
        }
        
        switch (heatingInfo_m.advanceStatus)
        {
            case ADVANCE_AS_SCHEDULED:
                lcd_m.print(" ");
                break;
            
            case ADVANCE_PLUS_ONE_HOUR:
                lcd_m.print("+");
                break;
                
            case ADVANCED_TO_NEXT_EVENT:
                lcd_m.print(RIGHT_ARROW_SYMBOL);
                break;
            
            default:
                lcd_m.print("?");
                break;
        }
               
        lcd_m.setCursor(0, 1);
        
        if (UI_STATE_SYNCHRONISING == yUIState_m)
        {
            GET_PROGMEM_STRING(MSG_LCD_SYNCHRONISING);  // lcd_m.print("Synchronising...");
            lcd_m.print(vsProgmemBuffer_m);            
        }
        if (UI_STATE_SYNCHRONISATION_COMPLETE == yUIState_m)
        {
            GET_PROGMEM_STRING(MSG_LCD_SYNCHRONISED);  // lcd_m.print("Synchronised!   ");
            lcd_m.print(vsProgmemBuffer_m);
            
        }
        if (UI_STATE_SYNCHRONISATION_FAILED == yUIState_m)
        {
            GET_PROGMEM_STRING(MSG_LCD_TIMED_OUT);  // lcd_m.print("Timed out       ");   
            lcd_m.print(vsProgmemBuffer_m);
            
        }
        else
        {            
            lcd_m.print(heatingInfo_m.messageOfTheDay);      
        }
    }
    else if (UI_STATE_MENU_1_MAX_TEMPERATURE == yUIState_m)
    {
        GET_PROGMEM_STRING(MSG_LCD_SET_MAX_TEMP_QUESTION);  // "Set max temp?"
        lcd_m.print(vsProgmemBuffer_m);
        lcd_m.setCursor(0, 1);
        GET_PROGMEM_STRING(MSG_LCD_CURRENT_TEMP);  // "Current:  "
        lcd_m.print(vsProgmemBuffer_m);
        lcd_m.print(heatingInfo_m.maxTemp, DEC);
        lcd_m.print(DEGREES_SYMBOL);        
    }
    else if (UI_STATE_MENU_2_APPLIANCES == yUIState_m)
    {
        GET_PROGMEM_STRING(MSG_LCD_SET_MODE_QUESTION);  // "Set mode?"
        lcd_m.print(vsProgmemBuffer_m);
        lcd_m.setCursor(0, 1);
        GET_PROGMEM_STRING(MSG_LCD_CURRENT_MODE);  // "Current:  "
        lcd_m.print(vsProgmemBuffer_m);
        if (APPLIANCES_NONE == heatingInfo_m.appliances)
        {
            GET_PROGMEM_STRING(MSG_LCD_MODE_OFF);  // "Off" 
            lcd_m.print(vsProgmemBuffer_m);
        }
        else if (APPLIANCES_HOT_WATER_ONLY == heatingInfo_m.appliances)
        {
            GET_PROGMEM_STRING(MSG_LCD_MODE_HW);  // "HW"
            lcd_m.print(vsProgmemBuffer_m);
        }
        else if (APPLIANCES_HOT_WATER_AND_CENTRAL_HEATING == heatingInfo_m.appliances)
        {
            GET_PROGMEM_STRING(MSG_LCD_MODE_HW_CH);  // "HW&CH"
            lcd_m.print(vsProgmemBuffer_m);
        }        
    }
    else if (yUIState_m >= UI_STATE_MENU_3_WINDOW_TIMES_START  &&  yUIState_m <= UI_STATE_MENU_3_WINDOW_TIMES_END)
    {
        byte yDay = (byte) ((yUIState_m - UI_STATE_MENU_3_WINDOW_TIMES_START) / 3);
        byte yWindow = (byte) ((yUIState_m - UI_STATE_MENU_3_WINDOW_TIMES_START) % 3);
        byte yStartTime = heatingInfo_m.startWindows[yDay][yWindow];
        byte yEndTime = heatingInfo_m.endWindows[yDay][yWindow];
        
        GET_PROGMEM_STRING(MSG_LCD_SET_WINDOW_QUESTION_1);  // "Set "
        lcd_m.print(vsProgmemBuffer_m);
        lcd_m.print(getLongDayName(yDay));
        lcd_m.print(" ");
        lcd_m.print(yWindow+1, DEC);
        
        GET_PROGMEM_STRING(MSG_LCD_SET_WINDOW_QUESTION_2);  // " times?"
        lcd_m.print(vsProgmemBuffer_m);
        lcd_m.setCursor(0, 1);
        lcd_m.print(" ");
        if (yStartTime < 100)
        {
            lcd_m.print("0");
        }
        lcd_m.print(int(yStartTime/10), DEC);
        lcd_m.print(":");
        lcd_m.print(yStartTime % 10, DEC);
        lcd_m.print("0");
        
        GET_PROGMEM_STRING(MSG_LCD_SET_WINDOW_TO);  // " to "
        lcd_m.print(vsProgmemBuffer_m);
        if (yEndTime < 100)
        {
            lcd_m.print("0");
        }
        lcd_m.print(int(yEndTime/10), DEC);
        lcd_m.print(":");        
        lcd_m.print(yEndTime % 10, DEC);
        lcd_m.print("0");
    }
    else if (UI_STATE_MENU_4_CLOCK == yUIState_m)
    {
           // TODO - continue the progmem settings here
        lcd_m.print("Set clock?");
        lcd_m.setCursor(0, 1);
        lcd_m.print("  ");
        lcd_m.print(getLongDayName(heatingInfo_m.currentDay));        
        lcd_m.print("  ");
        if (heatingInfo_m.currentTimeHours < 10)
        {
            lcd_m.print("0");
        }
        lcd_m.print(heatingInfo_m.currentTimeHours, DEC);
        lcd_m.print(":");
        if (heatingInfo_m.currentTimeMins < 10)
        {
            lcd_m.print("0");
        }
        lcd_m.print(heatingInfo_m.currentTimeMins, DEC);
    }
    else if (UI_STATE_MENU_5_DISPLAY_LAST_SYNC_TIME == yUIState_m)
    {
        unsigned long lMillisSinceLastSync = millisSince(lLastSynchroniseSuccessTime_m);
        unsigned long lMinsSinceLastSync = lMillisSinceLastSync / 60000L;   
        
        GET_PROGMEM_STRING(MSG_LCD_LAST_SYNC_WAS);  // lcd_m.print("Last sync was");        
        lcd_m.print(vsProgmemBuffer_m);
        lcd_m.setCursor(0, 1);
        
        if (0 == lLastSynchroniseSuccessTime_m)
        {            
            GET_PROGMEM_STRING(MSG_LCD_LAST_SYNC_NEVER);  // lcd_m.print("never!");
            lcd_m.print(vsProgmemBuffer_m);
            
        }
        else if (lMinsSinceLastSync > 999999)
        {
            GET_PROGMEM_STRING(MSG_LCD_LAST_SYNC_AGES_AGO);  // lcd_m.print("ages ago");
            lcd_m.print(vsProgmemBuffer_m);
        }
        else
        {
            lcd_m.print(lMinsSinceLastSync, DEC);
            GET_PROGMEM_STRING(MSG_LCD_LAST_SYNC_MINUTES_AGO);  // lcd_m.print(" mins ago");
            lcd_m.print(vsProgmemBuffer_m);
        }        
    }
    else if (UI_STATE_MENU_6_TOGGLE_SYNC == yUIState_m)
    {
        GET_PROGMEM_STRING(MSG_LCD_INTERNET_SYNC);  // lcd_m.print("Internet sync");
        lcd_m.print(vsProgmemBuffer_m);
        lcd_m.setCursor(0, 1);
        GET_PROGMEM_STRING(MSG_LCD_INTERNET_SYNC_ENABLED_QUESTION);  // lcd_m.print("enabled? ");        
        lcd_m.print(vsProgmemBuffer_m);
        if (heatingInfo_m.internetSyncEnabled)
        {
            GET_PROGMEM_STRING(MSG_LCD_INTERNET_SYNC_ENABLED_YES);  // "YES"
        }
        else
        {
            GET_PROGMEM_STRING(MSG_LCD_INTERNET_SYNC_ENABLED_NO);  // "NO"
        }
        lcd_m.print(vsProgmemBuffer_m);
    }
    else if (UI_STATE_MENU_7_SYNC_DELAY == yUIState_m)
    {        
        GET_PROGMEM_STRING(MSG_LCD_SET_SYNC_DELAY_QUESTION);  // lcd_m.print("Set sync delay?");
        lcd_m.print(vsProgmemBuffer_m);        
        lcd_m.setCursor(0, 1);
        GET_PROGMEM_STRING(MSG_LCD_SET_SYNC_DELAY_CURRENT);  // lcd_m.print("Current: ");
        lcd_m.print(vsProgmemBuffer_m);        
        lcd_m.print(int(lMaxInternetSyncDelayMillis_m/1000), DEC);        
        lcd_m.print("s");
    }
    else if (UI_STATE_EDIT_CLOCK_1_DAY_OF_WEEK == yUIState_m)
    {
        GET_PROGMEM_STRING(MSG_LCD_SET_CLOCK);  // lcd_m.print("Set clock");     
        lcd_m.print(vsProgmemBuffer_m);        
        lcd_m.setCursor(0, 1);
        lcd_m.print(" [");
        lcd_m.print(getLongDayName(heatingInfo_m.currentDay));        
        lcd_m.print("] ");
        if (heatingInfo_m.currentTimeHours < 10)
        {
            lcd_m.print("0");
        }
        lcd_m.print(heatingInfo_m.currentTimeHours, DEC);
        lcd_m.print(":");
        if (heatingInfo_m.currentTimeMins < 10)
        {
            lcd_m.print("0");
        }
        lcd_m.print(heatingInfo_m.currentTimeMins, DEC);
    }    
    else if (UI_STATE_EDIT_CLOCK_2_HOUR == yUIState_m)
    {
        GET_PROGMEM_STRING(MSG_LCD_SET_CLOCK);  // lcd_m.print("Set clock");     
        lcd_m.print(vsProgmemBuffer_m);        
        lcd_m.setCursor(0, 1);
        lcd_m.print("  ");
        lcd_m.print(getLongDayName(heatingInfo_m.currentDay));        
        lcd_m.print(" [");
        if (heatingInfo_m.currentTimeHours < 10)
        {
            lcd_m.print("0");
        }
        lcd_m.print(heatingInfo_m.currentTimeHours, DEC);
        lcd_m.print("]");
        if (heatingInfo_m.currentTimeMins < 10)
        {
            lcd_m.print("0");
        }
        lcd_m.print(heatingInfo_m.currentTimeMins, DEC);
    }    
    else if (UI_STATE_EDIT_CLOCK_3_MINUTES == yUIState_m)
    {
        GET_PROGMEM_STRING(MSG_LCD_SET_CLOCK);  // lcd_m.print("Set clock");     
        lcd_m.print(vsProgmemBuffer_m);        
        lcd_m.setCursor(0, 1);
        lcd_m.print("  ");
        lcd_m.print(getLongDayName(heatingInfo_m.currentDay));        
        lcd_m.print("  ");
        if (heatingInfo_m.currentTimeHours < 10)
        {
            lcd_m.print("0");
        }
        lcd_m.print(heatingInfo_m.currentTimeHours, DEC);
        lcd_m.print("[");
        if (heatingInfo_m.currentTimeMins < 10)
        {
            lcd_m.print("0");
        }
        lcd_m.print(heatingInfo_m.currentTimeMins, DEC);   
        lcd_m.print("]");
    }    
    else if (UI_STATE_EDIT_MAX_TEMPERATURE == yUIState_m)
    {     
        GET_PROGMEM_STRING(MSG_LCD_SET_MAX_TEMP);  // lcd_m.print("Set max temp");
        lcd_m.print(vsProgmemBuffer_m);        
        lcd_m.setCursor(0, 1);
        GET_PROGMEM_STRING(MSG_LCD_SET_MAX_TEMP_NEW);  // lcd_m.print("New:     [");
        lcd_m.print(vsProgmemBuffer_m);        
        lcd_m.print(heatingInfo_m.maxTemp, DEC);
        lcd_m.print(DEGREES_SYMBOL);   
        lcd_m.print("]");
    }
    else if (UI_STATE_EDIT_APPLIANCES == yUIState_m)
    {
        GET_PROGMEM_STRING(MSG_LCD_SET_MODE);  // lcd_m.print("Set mode");
        lcd_m.print(vsProgmemBuffer_m);        
        lcd_m.setCursor(0, 1);
        GET_PROGMEM_STRING(MSG_LCD_SET_MODE_NEW);  // lcd_m.print("New:     [");
        lcd_m.print(vsProgmemBuffer_m);        
        if (APPLIANCES_NONE == heatingInfo_m.appliances)
        {
            GET_PROGMEM_STRING(MSG_LCD_SET_MODE_OFF);  // lcd_m.print("Off  ");
            lcd_m.print(vsProgmemBuffer_m);                
        }
        else if (APPLIANCES_HOT_WATER_ONLY == heatingInfo_m.appliances)
        {
            GET_PROGMEM_STRING(MSG_LCD_SET_MODE_HW);  // lcd_m.print("HW   ");
            lcd_m.print(vsProgmemBuffer_m);                
        }
        else if (APPLIANCES_HOT_WATER_AND_CENTRAL_HEATING == heatingInfo_m.appliances)
        {
            GET_PROGMEM_STRING(MSG_LCD_SET_MODE_BOTH);  // lcd_m.print("HW&CH");
            lcd_m.print(vsProgmemBuffer_m);                
        }
        lcd_m.print("]");
    }
    else if (UI_STATE_EDIT_SYNC_DELAY == yUIState_m)
    {        
        GET_PROGMEM_STRING(MSG_LCD_SET_SYNC_DELAY);  // lcd_m.print("Set sync delay");
        lcd_m.print(vsProgmemBuffer_m);                 
        lcd_m.setCursor(0, 1);        
        GET_PROGMEM_STRING(MSG_LCD_SET_SYNC_DELAY_NEW);  // lcd_m.print("New:    [");
        lcd_m.print(vsProgmemBuffer_m);                 
        lcd_m.print(int(lMaxInternetSyncDelayMillis_m/1000), DEC);        
        lcd_m.print("s]");   
    }
    else if (yUIState_m >= UI_STATE_EDIT_WINDOW_TIMES_START  &&  yUIState_m <= UI_STATE_EDIT_WINDOW_TIMES_END)
    {
        int iIndex = yUIState_m - UI_STATE_EDIT_WINDOW_TIMES_START;
        //
        //  0   Day 0   Window 0    Start
        //  1   Day 0   Window 0    End
        //  2   Day 0   Window 1    Start
        //  3   Day 0   Window 1    End  
        //  4   Day 0   Window 2    Start
        //  5   Day 0   Window 2    End  
        //  6   Day 1   Window 0    Start
        //  7   Day 1   Window 0    Start
        //
        byte yDay = (byte) (iIndex / 6);
        byte yWindow = (byte) ((iIndex % 6) / 2);        
        bool isStartTime = (0 == (iIndex % 2));
        byte yStartTime = heatingInfo_m.startWindows[yDay][yWindow];
        byte yEndTime = heatingInfo_m.endWindows[yDay][yWindow];
        
        GET_PROGMEM_STRING(MSG_LCD_SET_WINDOW_TIMES_1);  // lcd_m.print("Set ");
        lcd_m.print(vsProgmemBuffer_m);                 
        lcd_m.print(getLongDayName(yDay));
        lcd_m.print(" ");
        lcd_m.print(yWindow+1, DEC);        
        GET_PROGMEM_STRING(MSG_LCD_SET_WINDOW_TIMES_2);  // lcd_m.print(" times");
        lcd_m.print(vsProgmemBuffer_m);                 
        lcd_m.setCursor(0, 1);
        
        if (isStartTime)
        {
            lcd_m.print("[");
            if (yStartTime < 100)
            {
                lcd_m.print("0");
            }
            lcd_m.print(int(yStartTime / 10), DEC);
            lcd_m.print(":");
            lcd_m.print(yStartTime % 10, DEC);
            lcd_m.print("0]to ");           
            if (yEndTime < 100)
            {
                lcd_m.print("0");
            }
            lcd_m.print(int(yEndTime / 10), DEC);
            lcd_m.print(":");
            lcd_m.print(yEndTime % 10, DEC);
            lcd_m.print("0");
        }
        else
        {
            lcd_m.print(" ");
            if (yStartTime < 100)
            {
                lcd_m.print("0");
            }
            lcd_m.print(int(yStartTime / 10), DEC);
            lcd_m.print(":");
            lcd_m.print(yStartTime % 10, DEC);
            lcd_m.print("0 to[");           
            if (yEndTime < 100)
            {
                lcd_m.print("0");
            }            
            lcd_m.print(int(yEndTime / 10), DEC);
            lcd_m.print(":");
            lcd_m.print(yEndTime % 10, DEC);
            lcd_m.print("0]");
        }
    }
    else
    {
        // ERROR?
        GET_PROGMEM_STRING(MSG_LCD_UNKNOWN_UI_STATE);  // lcd_m.print("Unknown UI state");
        lcd_m.print(vsProgmemBuffer_m);                 
        lcd_m.setCursor(0,1);
        lcd_m.print(yUIState_m, DEC);
        delay(3000);
        setUIState(UI_STATE_NORMAL, false);
    }
}


int readCharsFromWifly(char *pBuffer_p, int iMaxChars_p, unsigned long lTimeout_p)
{
    unsigned long lStartTime = millis();
    
    bool isTimedOut = false;
    bool isMaxCharsReached = false;
    int iCharsReadSoFar = 0;
    
    while (! isTimedOut  &&  ! isMaxCharsReached)
    {
        while (Serial.available() > 0)
        {
            char c = Serial.read();
            pBuffer_p[iCharsReadSoFar] = c;
            iCharsReadSoFar++;
            
            if (iCharsReadSoFar == iMaxChars_p)
            {
                isMaxCharsReached = true;
                break;
            }
        }
     
        if (millisSince(lStartTime) > lTimeout_p)
        {
            isTimedOut = true;
        }   
    }
    
    pBuffer_p[iCharsReadSoFar] = 0;  // Null terminate
    return iCharsReadSoFar;
}


void setup()   
{   
    heatingInfo_m.internetSyncEnabled = true;
    heatingInfo_m.maxTemp = 21;
    heatingInfo_m.currentTimeHours = 12;
    heatingInfo_m.appliances = APPLIANCES_HOT_WATER_AND_CENTRAL_HEATING;
    heatingInfo_m.isAmbientTempSet = false;
    
    for (int iDay = 0; iDay < DAYS_IN_A_WEEK; iDay++)
    {
        heatingInfo_m.startWindows[iDay][0] = 60;
        heatingInfo_m.endWindows[iDay][0] = 63;
        heatingInfo_m.startWindows[iDay][1] = 160;
        heatingInfo_m.endWindows[iDay][1] = 170;        
    }
    
    // Different times for sat/sun
    /*
    heatingInfo_m.startWindows[0][0] = 80;
    heatingInfo_m.endWindows[0][0] = 83;
    heatingInfo_m.startWindows[0][1] = 160;
    heatingInfo_m.endWindows[0][1] = 163;
    */    
    heatingInfo_m.startWindows[0][0] = 121; // These are just for easy testing...
    heatingInfo_m.endWindows[0][0] = 122;
    heatingInfo_m.startWindows[0][1] = 124;
    heatingInfo_m.endWindows[0][1] = 125;
    
    heatingInfo_m.startWindows[1][0] = 80;
    heatingInfo_m.endWindows[1][0] = 83;
    heatingInfo_m.startWindows[1][1] = 160;
    heatingInfo_m.endWindows[1][1] = 163;        
                 
    pinMode(ADVANCE_BUTTON_PIN, INPUT);
    pinMode(ENCODER_BUTTON_PIN, INPUT);
    //pinMode(BACK_BUTTON_PIN, INPUT);
    pinMode(TEMPERATURE_SENSOR_PIN, INPUT);    
    pinMode(HEATING_RELAY_PIN, OUTPUT);
    pinMode(WATER_RELAY_PIN, OUTPUT);
    
    for (byte yGPIO = 0; yGPIO < NUM_GPIO_PINS; yGPIO++)
    {
        pinMode(GPIO_PINS[yGPIO], OUTPUT);
        digitalWrite(GPIO_PINS[yGPIO], LOW);   
    }
    
    digitalWrite(TEMPERATURE_SENSOR_PIN, LOW); //- setting this to HIGH or LOW doesn't seem to have any effect at all
    
    // Flash LEDs to show we mean business
    digitalWrite(WATER_RELAY_PIN, HIGH);
    delay(500);
    digitalWrite(WATER_RELAY_PIN, LOW);
    digitalWrite(HEATING_RELAY_PIN, HIGH);
    delay(500);
    digitalWrite(HEATING_RELAY_PIN, LOW);
    
    lcd_m.begin(16, 2);    
    lcd_m.clear();        
    GET_PROGMEM_STRING(MSG_LCD_HELLO);  // lcd_m.print("Hello");
    lcd_m.print(vsProgmemBuffer_m);                 
    
    
    
    encoder_m.write(0);

    realSerial_m.begin(57600);
    realSerial_m.println();
    GET_PROGMEM_STRING(MSG_SERIALDEBUG_INIT);  // realSerial_m.println("Serial init..");        
    realSerial_m.println(vsProgmemBuffer_m);                 
    
                
    updateDisplay(false);    
    
    wifly_m.begin(9600, 3000, &realSerial_m);    
    synchroniseWithInternet(true);
}




void loop()                     
{
    /*
    if (UI_STATE_NORMAL == yUIState_m  &&  millisSince(lLastTimeCheckTime_m) > 60000UL)
    {
        lLastTimeCheckTime_m = millis();
        // Increment the time - just a joke function for now
        if (heatingInfo_m.currentTimeMins >= 59)
        {            
            heatingInfo_m.currentTimeMins = 0;
            
            if (heatingInfo_m.currentTimeHours >= 23)
            {
                heatingInfo_m.currentTimeHours = 0;
                heatingInfo_m.currentDay = (heatingInfo_m.currentDay + 1) % DAYS_IN_A_WEEK;
            }
            else
            {
                heatingInfo_m.currentTimeHours++;
            }
        }
        else
        {
            heatingInfo_m.currentTimeMins++;
        }
        
        updateRelays();
        updateDisplay(false);
    }   
    */
    if (! digitalRead(ADVANCE_BUTTON_PIN))  // It's normally high - pushing the button grounds it.
    {
        isHeatingInfoChangedLocally_m = true;
        heatingInfo_m.advanceStatus = (heatingInfo_m.advanceStatus + 1) % 3;
        heatingInfo_m.lastAdvanceStatusChangeMillis = millis();
        updateDisplay(false);
        updateRelays();
        waitForDebounce(ADVANCE_BUTTON_PIN);
    }

    if (! digitalRead(ENCODER_BUTTON_PIN))  //  It's normally high - pushing the button grounds it.
    {
        if (UI_STATE_NORMAL == yUIState_m  ||  UI_STATE_SYNCHRONISING == yUIState_m  ||  UI_STATE_SYNCHRONISATION_COMPLETE == yUIState_m  ||  UI_STATE_SYNCHRONISATION_FAILED == yUIState_m)
        {
            // Do nothing   
        }
        else if (UI_STATE_MENU_1_MAX_TEMPERATURE == yUIState_m)
        {
            setUIState(UI_STATE_EDIT_MAX_TEMPERATURE, false);
        }
        else if (UI_STATE_MENU_2_APPLIANCES == yUIState_m)
        {
            setUIState(UI_STATE_EDIT_APPLIANCES, false);
        }
        else if (yUIState_m >= UI_STATE_MENU_3_WINDOW_TIMES_START  &&  yUIState_m <= UI_STATE_MENU_3_WINDOW_TIMES_END)
        {
            byte yDay = (byte) ((yUIState_m - UI_STATE_MENU_3_WINDOW_TIMES_START) / 3);
            byte yWindow = (byte) ((yUIState_m - UI_STATE_MENU_3_WINDOW_TIMES_START) % 3);
     
            setUIState(UI_STATE_EDIT_WINDOW_TIMES_START + (yDay * 6) + (yWindow * 2), false);
        }
        else if (UI_STATE_MENU_4_CLOCK == yUIState_m)
        {
            setUIState(UI_STATE_EDIT_CLOCK_1_DAY_OF_WEEK, false);
        }
        else if (UI_STATE_MENU_5_DISPLAY_LAST_SYNC_TIME == yUIState_m)
        {
            setUIState(UI_STATE_EDIT_CLOCK_1_DAY_OF_WEEK, false);
        }
        else if (UI_STATE_MENU_6_TOGGLE_SYNC == yUIState_m)
        {
            heatingInfo_m.internetSyncEnabled = ! heatingInfo_m.internetSyncEnabled;
            isHeatingInfoChangedLocally_m = true;
            updateDisplay(true);
        }
        else if (UI_STATE_MENU_7_SYNC_DELAY == yUIState_m)
        {
            setUIState(UI_STATE_EDIT_SYNC_DELAY, false);   
        }
        else if (UI_STATE_EDIT_MAX_TEMPERATURE == yUIState_m)
        {
            setUIState(UI_STATE_NORMAL, false);
        }
        else if (UI_STATE_EDIT_APPLIANCES == yUIState_m)
        {
            setUIState(UI_STATE_NORMAL, false);
        }
        else if (UI_STATE_EDIT_SYNC_DELAY == yUIState_m)
        {
            setUIState(UI_STATE_NORMAL, false);   
        }
        else if (UI_STATE_EDIT_CLOCK_1_DAY_OF_WEEK == yUIState_m)
        {
            setUIState(UI_STATE_EDIT_CLOCK_2_HOUR, false);
        }
        else if (UI_STATE_EDIT_CLOCK_2_HOUR == yUIState_m)
        {
            setUIState(UI_STATE_EDIT_CLOCK_3_MINUTES, false);
        }
        else if (UI_STATE_EDIT_CLOCK_3_MINUTES == yUIState_m)
        {
            setUIState(UI_STATE_NORMAL, false);
        }
        else if (yUIState_m >= UI_STATE_EDIT_WINDOW_TIMES_START  &&  yUIState_m <= UI_STATE_EDIT_WINDOW_TIMES_END)
        {
            bool isStartTime = (0 == (yUIState_m % 2));
            
            if (isStartTime)
            {
                setUIState(yUIState_m+1, false);
            }
            else
            {
                setUIState(UI_STATE_NORMAL, false);
            }
        }   
        else
        {
            // Unknown state?
        }
        
        waitForDebounce(ENCODER_BUTTON_PIN);
    }
    
    /*
    if (digitalRead(BACK_BUTTON_PIN))
    {
        setUIState(yPreviousUIState_m);
        yPreviousUIState_m = UI_STATE_NORMAL;
        updateDisplay(false);
        
        waitForDebounce(BACK_BUTTON_PIN);
    }
    */

    int iOldEncoderValue = iEncoderValue_m;
    int iNewEncoderValue = encoder_m.read();
    
    if (abs(iNewEncoderValue - iOldEncoderValue) >= 3)
    {
        iEncoderValue_m = iNewEncoderValue;
        int iEncoderDifference = (iNewEncoderValue - iOldEncoderValue) / 3;
        
        // Do something   
        if (yUIState_m >= UI_STATE_MENU_MIN  &&  yUIState_m <= UI_STATE_MENU_MAX)
        {            
            // Just rotate around the menus 
            setUIState(constrain(yUIState_m + iEncoderDifference, UI_STATE_MENU_MIN, UI_STATE_MENU_MAX), false);
        }
        else if (UI_STATE_EDIT_MAX_TEMPERATURE == yUIState_m)
        {
            // Twiddle temp
            heatingInfo_m.maxTemp = constrain(heatingInfo_m.maxTemp + iEncoderDifference, 0, 30);            
            isHeatingInfoChangedLocally_m = true;
            updateDisplay(true);
            // No need to update relays as it'll happen when you go back to NORMAL UI state
        }
        else if (UI_STATE_EDIT_APPLIANCES == yUIState_m)
        {
            // Twiddle appliances
            heatingInfo_m.appliances = constrain(heatingInfo_m.appliances + iEncoderDifference, 0, APPLIANCES_MAX);
            isHeatingInfoChangedLocally_m = true;
            updateDisplay(true);            
            // No need to update relays as it'll happen when you go back to NORMAL UI state
        }
        else if (UI_STATE_EDIT_SYNC_DELAY == yUIState_m)
        {
            // Twiddle delay
            
            lMaxInternetSyncDelayMillis_m = constrain(lMaxInternetSyncDelayMillis_m + (iEncoderDifference*10000UL), 10000UL, 3600000UL);
            updateDisplay(true);               
        }
        else if (UI_STATE_EDIT_CLOCK_1_DAY_OF_WEEK == yUIState_m)
        {
            // Twiddle day
            heatingInfo_m.currentDay = constrain(heatingInfo_m.currentDay + iEncoderDifference, 0, DAYS_IN_A_WEEK-1);
            isHeatingInfoChangedLocally_m = true;
            updateDisplay(true);
        }
        else if (UI_STATE_EDIT_CLOCK_2_HOUR == yUIState_m)
        {
            // Twiddle hour
            heatingInfo_m.currentTimeHours = constrain(heatingInfo_m.currentTimeHours + iEncoderDifference, 0, 23);
            isHeatingInfoChangedLocally_m = true;
            updateDisplay(true);
        }
        else if (UI_STATE_EDIT_CLOCK_3_MINUTES == yUIState_m)
        {
            // Twiddle mins
            heatingInfo_m.currentTimeMins = constrain(heatingInfo_m.currentTimeMins + iEncoderDifference, 0, 59);
            isHeatingInfoChangedLocally_m = true;
            updateDisplay(true);
        }
        else if (yUIState_m >= UI_STATE_EDIT_WINDOW_TIMES_START  &&  yUIState_m <= UI_STATE_EDIT_WINDOW_TIMES_END)
        {   
            // Figure out which one it is, update something, and refresh the LCD
            // This first bit is the same as in the LCD update method
            int iIndex = yUIState_m - UI_STATE_EDIT_WINDOW_TIMES_START;
            byte yDay = (byte) (iIndex / 6);
            byte yWindow = (byte) ((iIndex % 6) / 2);        
            bool isStartTime = (0 == (iIndex % 2));
            
            int iOriginalValue;
            int iNewValue;
            
            if (isStartTime)
            {
                iOriginalValue = (int) heatingInfo_m.startWindows[yDay][yWindow];                
            }
            else
            {
                iOriginalValue = (int) heatingInfo_m.endWindows[yDay][yWindow];
            }
            
            // convert iOriginalValue into minutes
            int iHours = int(iOriginalValue / 10);
            int iMinutes = (iOriginalValue % 10) * 10;
            int iTotalMinutes = (iHours * 60) + iMinutes;
            // num mins from 00:00 to 24:00 is 24 * 60 + 1 = 1441
            iTotalMinutes = (iTotalMinutes + (iEncoderDifference * 10)) % (MINUTES_IN_A_DAY+1);
            
            // convert total minutes back into a char (e.g. 235 = 23:50)
            iHours = int(iTotalMinutes / 60);
            iMinutes = int(iTotalMinutes % 60);
            iNewValue = (iHours * 10) + int(iMinutes / 10);           
            iNewValue = constrain(iNewValue, 0, 240); 
            
            if (isStartTime)
            {
                heatingInfo_m.startWindows[yDay][yWindow] = (char) iNewValue;
                
                if (heatingInfo_m.endWindows[yDay][yWindow] < (char) iNewValue)
                {
                    heatingInfo_m.endWindows[yDay][yWindow] = (char) iNewValue;
                }
            }
            else
            {
                heatingInfo_m.endWindows[yDay][yWindow] = (char) iNewValue;
            }
            
            isHeatingInfoChangedLocally_m = true;
            updateDisplay(true);
        }
        
    }


    // Check if the temperature has changed and, if so, how long it's been changed for
    // Take average of last 3 temp ratings over 1 minute each.  
    //Temp is 1 below/higher than max then wait a minute and try again.  if it's still 1 below/higher (same sign) then assume that's right.    
    
    
    if (millisSince(lLastTemperatureSampleTime_m) > TEMPERATURE_SAMPLE_DELAY_MILLIS  ||  !heatingInfo_m.isAmbientTempSet)
    {        
        double dTemperatureSample = analogRead(5);
        
        realSerial_m.print("Temp sample:");
        realSerial_m.println(dTemperatureSample, DEC);
        
        
        //int iTemperatureCentigrade = int(iTemperatureSample); // / 10) - 12; // TEMP
        //T = (3,500,000 / ((10,000 / Vin) + 30,000)) - (45 + IF(
        //=(3500000 / ((10000/C4)+30000))-(45+IF(C4 < 0.5, 3, IF(C4 > 0.5, -4, 0)))
        
        double dVoltageProportion = dTemperatureSample / 1024.0;
        
        double dExtra = 0.0;
        if (dVoltageProportion <= 0.4)
        {
            dExtra = -3;
        }
        else if (dVoltageProportion >= 0.6)
        {
            dExtra = -4;
        }
         
        //int iTemperatureCentigrade = int(((3500000 / ((10000/dVoltageProportion)+30000)) + dVoltageProportion) - (45));
        
        
        
        
        for (byte i=0; i < MAX_TEMPERATURE_SAMPLES-1; i++)
        {
            if (0 == lLastTemperatureSampleTime_m)
            {
                temperatureSamples_m[i] = int(dTemperatureSample);  // First time, just fill with the current value
            }
            else
            {
                temperatureSamples_m[i] = temperatureSamples_m[i+1];  // Roll back towards index [0]
            }             
        }
        
        temperatureSamples_m[MAX_TEMPERATURE_SAMPLES-1] = int(dTemperatureSample);
        
        int iAverageTemperatureReading = 0;
        
        for (byte i=0; i < MAX_TEMPERATURE_SAMPLES; i++)
        {
            iAverageTemperatureReading += temperatureSamples_m[i];    
        }
        
        
        
        iAverageTemperatureReading /= MAX_TEMPERATURE_SAMPLES;
        
        double dTemp;
        dTemp = log(((10240000/iAverageTemperatureReading) - 10000));
        dTemp = 1 / (0.001129148 + (0.000234125 + (0.0000000876741 * dTemp * dTemp ))* dTemp );
        dTemp = dTemp - 273.15;            // Convert Kelvin to Celcius
        int iTemperatureCentigrade = int(dTemp);

//        int iTemperatureCentigrade = int(iAverageTemperatureReading / 100);  // TODO - convert analog 0-1024 input into degrees centigrade        
        
        

    
        bool isTemperatureChanged = (iTemperatureCentigrade != heatingInfo_m.ambientTemp);        
        heatingInfo_m.ambientTemp = iTemperatureCentigrade;
        heatingInfo_m.isAmbientTempSet = true;
        lLastTemperatureSampleTime_m = millis();
        
        if (isTemperatureChanged)
        {            
            updateDisplay(false);
            updateRelays();
        }
    }
    

    if (UI_STATE_NORMAL != yUIState_m  &&  UI_STATE_SYNCHRONISING != yUIState_m  &&  UI_STATE_SYNCHRONISATION_COMPLETE != yUIState_m)
    {
        if (millisSince(lLastUIStateChangeTime_m) > MAX_UI_STATE_IDLE_TIME_MILLIS)
        {
            setUIState(UI_STATE_NORMAL, false);
        }
    }
    
    // TODO - every minute, call onto the wifly clock, then set/unset relays
    // TODO - every few minutes or so, try to synchronise with internets, then set/unset relays

    
  
    if (UI_STATE_NORMAL == yUIState_m  ||  UI_STATE_SYNCHRONISING == yUIState_m  ||  UI_STATE_SYNCHRONISATION_COMPLETE == yUIState_m)
    {
        if (heatingInfo_m.internetSyncEnabled  &&  millisSince(lLastSynchroniseSendTime_m) > lMaxInternetSyncDelayMillis_m)
        {            
            realSerial_m.print(lMaxInternetSyncDelayMillis_m, DEC);
            realSerial_m.println("Resync!");
            synchroniseWithInternet(false);                
        }
    }
}









//
//  Takes the line of text that was read from the HTTP response body and parses it into
//  individual fields.
//  The string is in the format:
//      
//      12112345123456789012345611
//      OK$hh:mmMessageOfTheeDay*&ABDCEFGHIJKLM... etc.
//
//      OKMessageOfTheDay %*&ABCDEF...
//
//  where:
//      '$' is the index of the weekday as an int e.g. "0" = "Sunday", "6" = "Saturday"
//      'hh:mm' is the current server time in 24 hour clock 
//      'MessageOfTheeDay' is an arbitrary 16-character string
//      '%' is the max temp as a one-char binary ASCII representation (with 100 added to it, e.g. 'd'=100="0C", 'x'=120="20C", 'Z'=90="-10C"
//      '*' is the appliances as an int (0=none, 1=hot water only, 2=hot water and central heating)
//      '&' is the advance status as an int (0=normal, 1=plus one hour, 2=advance)
//      '#' is the GPIO 1,2, and 3 status as a binary encoded int (0 = all off, 1 = GPIO1 on, 2 = GPIO2 on, 3 = GPIO 1&2 on, 7 = all 3 on).
//      'A', 'B', etc are binary representations of windows - grouped by day, with 3 windows per day and 2 events per day, 42 chars in total.
//      The binary representations are e.g. 101 for "10:00", 1 for "00:00", 236 for "23:50" (note we add 1 so that we don't send null chars).
//
//  There will be 26 bytes for everything before the windows, and 7*3*2 bytes (=42) for the windows, so that's 68 in total.
//
//
void parseHeatingInfo(char *psHttpBody_p)
{
    GET_PROGMEM_STRING(MSG_SERIALDEBUG_PARSING_HEATING_INFO);  // realSerial_m.print("Parsing");
    realSerial_m.print(vsProgmemBuffer_m);                 
    
    char* psOriginal = psHttpBody_p;
    /*
    if (0 != strncmp(psOriginal, "OK", 2))
    {
        strncpy(heatingInfo_m.messageOfTheDay, psOriginal, 16);
        return;   
    }    
    
    psOriginal += 2;
    */
    HeatingInfo newHeatingInfo;
    
    memset(&newHeatingInfo, 0, sizeof(newHeatingInfo));
    
    newHeatingInfo.currentDay = int((*psOriginal) - '0');    
    //strncpy(newHeatingInfo.currentDay, psOriginal, MAX_DAY_NAME_LENGTH);    
    psOriginal += 1;
    
    //newHeatingInfo.currentDay = getDayIndex(newHeatingInfo.currentDayName);  // TODO - this has been replaced with a single char
    
    newHeatingInfo.currentTimeHours = ((psOriginal[0] - '0') * 10) + (psOriginal[1] - '0');
    newHeatingInfo.currentTimeMins = ((psOriginal[3] - '0') * 10) + (psOriginal[4] - '0');    
    //strncpy(newHeatingInfo.currentDisplayTime, psOriginal, MAX_TIME_LENGTH);    
    psOriginal += MAX_TIME_LENGTH;
    
    // Convert the display time into a single char (like we use for the 'windows')
    //char szSingleCharTime[4];
    //szSingleCharTime[0] = newHeatingInfo.currentDisplayTime[0]; // first char of the hour
    //szSingleCharTime[1] = newHeatingInfo.currentDisplayTime[1]; // second char of the hour
    //szSingleCharTime[2] = newHeatingInfo.currentDisplayTime[3]; // first char of the minute
    //szSingleCharTime[3] = 0;
    //realSerial_m.print("XXX");
    //realSerial_m.print(szSingleCharTime);
    //realSerial_m.print("XXX");
    
    //newHeatingInfo.currentTime = (char) int(szSingleCharTime);    
    
    realSerial_m.print(newHeatingInfo.currentTimeHours, DEC);
    realSerial_m.print(":");
    realSerial_m.print(newHeatingInfo.currentTimeMins, DEC);
    realSerial_m.println();
    
    strncpy(newHeatingInfo.messageOfTheDay, psOriginal, MAX_MOTD_LENGTH);    
    psOriginal += MAX_MOTD_LENGTH;
    
    // Next char is the max temp, binary, with 100 added to it
    newHeatingInfo.maxTemp = int(*psOriginal) - 100;
    psOriginal++;
    
    // Next char is the appliances
    switch (psOriginal[0])
    {
        case '0':
            newHeatingInfo.appliances = APPLIANCES_NONE;
            break;       
            
        case '1':
            newHeatingInfo.appliances = APPLIANCES_HOT_WATER_ONLY;
            break;       
            
        default:
            newHeatingInfo.appliances = APPLIANCES_HOT_WATER_AND_CENTRAL_HEATING;
            break;
    }
        
    psOriginal++;
    
    // Next char is the advance status
    switch (psOriginal[0])
    {
        case '1':
            newHeatingInfo.advanceStatus = ADVANCE_PLUS_ONE_HOUR;
            break;
        
        case '2':
            newHeatingInfo.advanceStatus = ADVANCED_TO_NEXT_EVENT;
            break;
            
        default:
            newHeatingInfo.advanceStatus = ADVANCE_AS_SCHEDULED;
            break;
    }
        
    psOriginal++;
    
    int iGPIOStatuses = (*psOriginal) - '0';
    
    newHeatingInfo.gpioStatuses[0] = (iGPIOStatuses & 0x1) > 0;
    newHeatingInfo.gpioStatuses[1] = (iGPIOStatuses & 0x2) > 0;
    //newHeatingInfo.gpioStatuses[2] = (iGPIOStatuses & 0x4) > 0;    
    
    psOriginal++;
    
    // Now we have schedule windows for each day - 3 windows per day, 7 days
    for (int iDay = 0; iDay < DAYS_IN_A_WEEK; iDay++)
    {
        for (int iWindow = 0; iWindow < WINDOWS_PER_DAY; iWindow++)
        {
            // Binary data - one char for each start and each end window time
            // Note that we subtract 1 from the values because we were attempting
            // to avoid NULL (0) chars from making it look like it was the end of 
            // the string, so we had added 1 at the PHP stage.
            newHeatingInfo.startWindows[iDay][iWindow] = (*psOriginal)-1;
            psOriginal++;
            newHeatingInfo.endWindows[iDay][iWindow] = (*psOriginal)-1;
            psOriginal++;
            /*
            // Read groups of 3 characters
            char szWindow[4];
            strncpy(szWindow, psOriginal, 3);
            szWindow[3] = 0; // null terminate
            newHeatingInfo.startWindows[iDay][iWindow] = char(int(szWindow));
            psOriginal+=3;
            
            // Read groups of 3 characters
            strncpy(szWindow, psOriginal, 3);
            szWindow[3] = 0; // null terminate
            newHeatingInfo.endWindows[iDay][iWindow] = char(int(szWindow));
            psOriginal+=3;
            */
        }
    }
    
    realSerial_m.print("D:");
    realSerial_m.println(newHeatingInfo.currentDay, DEC);
    realSerial_m.print("T:");
    realSerial_m.print(newHeatingInfo.currentTimeHours, DEC);
    realSerial_m.print(":");
    realSerial_m.println(newHeatingInfo.currentTimeMins, DEC);
    realSerial_m.print("MOTD: ");
    realSerial_m.println(newHeatingInfo.messageOfTheDay);    
    realSerial_m.print("Apps:");
    realSerial_m.println(newHeatingInfo.appliances, DEC);
    realSerial_m.print("Adv: ");
    realSerial_m.println(newHeatingInfo.advanceStatus, DEC);
    
    for (int iGPIO = 0; iGPIO < NUM_GPIO_PINS; iGPIO++)
    {             
        GET_PROGMEM_STRING(MSG_SERIALDEBUG_GPIO_SETTING);  // realSerial_m.print("GPIO");
        realSerial_m.print(vsProgmemBuffer_m);    
        realSerial_m.print(iGPIO, DEC);
        realSerial_m.print(": ");
        realSerial_m.println(newHeatingInfo.gpioStatuses[iGPIO] ? "Y" : "N");
    }
    
    for (int iDay = 0; iDay < DAYS_IN_A_WEEK; iDay++)
    {        
        GET_PROGMEM_STRING(MSG_SERIALDEBUG_DAY_WINDOW_SETTING);  // realSerial_m.print("Day");
        realSerial_m.print(vsProgmemBuffer_m);    
        realSerial_m.print(iDay, DEC);
        realSerial_m.print(":");
        for (int iWindow = 0; iWindow < WINDOWS_PER_DAY; iWindow++)
        {
            realSerial_m.print(" ");
            realSerial_m.print(int(newHeatingInfo.startWindows[iDay][iWindow]), DEC);                        
            realSerial_m.print("-");                        
            realSerial_m.print(int(newHeatingInfo.endWindows[iDay][iWindow]), DEC);                        
        }
        realSerial_m.println();
    }       
    
    // Copy fields from one to the other
    if (heatingInfo_m.advanceStatus != newHeatingInfo.advanceStatus)
    {
        newHeatingInfo.lastAdvanceStatusChangeMillis = millis();
    }
            
    if (heatingInfo_m.advanceStatus != newHeatingInfo.advanceStatus)
    {
        heatingInfo_m.lastAdvanceStatusChangeMillis = millis();
    }
                        
    heatingInfo_m.currentDay = newHeatingInfo.currentDay;
    heatingInfo_m.maxTemp = newHeatingInfo.maxTemp;
    heatingInfo_m.currentTimeHours = newHeatingInfo.currentTimeHours;
    heatingInfo_m.currentTimeMins = newHeatingInfo.currentTimeMins;
    strncpy(heatingInfo_m.messageOfTheDay, newHeatingInfo.messageOfTheDay, MAX_MOTD_LENGTH);
    heatingInfo_m.appliances = newHeatingInfo.appliances;
    heatingInfo_m.advanceStatus = newHeatingInfo.advanceStatus;
    
    for (byte yGPIO = 0; yGPIO < NUM_GPIO_PINS; yGPIO++)
    {
        heatingInfo_m.gpioStatuses[yGPIO] = newHeatingInfo.gpioStatuses[yGPIO];
    }
    
    for (byte yDay = 0; yDay < DAYS_IN_A_WEEK; yDay++)
    {
        for (byte yWindow = 0; yWindow < WINDOWS_PER_DAY; yWindow++)
        {
            heatingInfo_m.startWindows[yDay][yWindow] = newHeatingInfo.startWindows[yDay][yWindow];
            heatingInfo_m.endWindows[yDay][yWindow] = newHeatingInfo.endWindows[yDay][yWindow];
        }
    }    
}




 


// 
//  Triggers an HTTP status update message to the website by simply sending 
//  data via the UART.  
//  The HTTP response will be read by the checkForWiflyOutput method later.
//  Normally it will send its current state to the server (so that the server
//  can be aware that the user has pressed some buttons on the physical box)
//  but, if the system is just starting up, it should pass the value 'true'
//  for the bForceSynchronise_p parameter so that it doesn't try to overwrite
//  what's on the server.
//
void synchroniseWithInternet(bool bForceSynchronise_p)
{   
    lLastSynchroniseSendTime_m = millis(); 
    setUIState(UI_STATE_SYNCHRONISING, false);    
    
    
    int iResult;
    
    if (bForceSynchronise_p)
    {
        GET_PROGMEM_STRING(MSG_WIFLY_STARTUP_MESSAGE);  // "STARTUP"
        iResult = wifly_m.sendHttpRequest(vsProgmemBuffer_m, HTTP_BODY_SEARCH_STRING, MAX_HTTP_BODY_CHARS_TO_READ, HTTP_TIMEOUT);
        //Serial.print("STARTUP");
        
    }
    else
    {
        // Buffer to store the non-Base64 encoded text = 4general + (7days * 3windows * 2times) chars = 46 chars
        int iSendBufferPos = 0;
        
        vsSendBuffer_m[iSendBufferPos++] = isHeatingInfoChangedLocally_m ? 'Y' : 'N';
        // Add 1 to these values so we don't inadvertently null-terminate the string!
        vsSendBuffer_m[iSendBufferPos++] = heatingInfo_m.appliances + 1;
        vsSendBuffer_m[iSendBufferPos++] = heatingInfo_m.advanceStatus + 1;
        vsSendBuffer_m[iSendBufferPos++] = heatingInfo_m.maxTemp + 1;
        
        for (byte yDay = 0; yDay < DAYS_IN_A_WEEK; yDay++)
        {
            for (byte yWindow = 0; yWindow < WINDOWS_PER_DAY; yWindow++)
            {
                // Add 1 to each numeric value (max would've been 240 == 24:00 anyway, so no
                // risk of overflows) so that we avoid sending '0' NULL characters.
                vsSendBuffer_m[iSendBufferPos++] = (heatingInfo_m.startWindows[yDay][yWindow]) + 1;
                vsSendBuffer_m[iSendBufferPos++] = (heatingInfo_m.endWindows[yDay][yWindow]) + 1;                                
            }
        }   
        
        vsSendBuffer_m[iSendBufferPos] = 0; // Null terminate
        
        // The data, once it's base64 encoded will be longer, around 70 bytes
        
        base64Encode(vsSendBuffer_m, vsBase64SendBuffer_m, MAX_BASE64_SEND_BUFFER_SIZE);
        
        strcat(vsBase64SendBuffer_m, "*");
        
        GET_PROGMEM_STRING(MSG_SERIALDEBUG_SENDING_1);  // "Sending ["        
        realSerial_m.print(vsProgmemBuffer_m);
        realSerial_m.print(vsSendBuffer_m);        
        GET_PROGMEM_STRING(MSG_SERIALDEBUG_SENDING_2);  // "], b64 ["
        realSerial_m.print(vsProgmemBuffer_m);
        realSerial_m.print(vsBase64SendBuffer_m);                
        GET_PROGMEM_STRING(MSG_SERIALDEBUG_SENDING_3);  // "]"
        realSerial_m.println(vsProgmemBuffer_m);
        
        iResult = wifly_m.sendHttpRequest(vsBase64SendBuffer_m, HTTP_BODY_SEARCH_STRING, MAX_HTTP_BODY_CHARS_TO_READ, HTTP_TIMEOUT);        
    }
    
    GET_PROGMEM_STRING(MSG_SERIALDEBUG_SENDING_RESULT);  // "Result "
    realSerial_m.print(vsProgmemBuffer_m);
    realSerial_m.println(iResult, DEC);
    
    
    if (HeatingWifly::SUCCESS == iResult)
    {
        parseHeatingInfo(wifly_m.getHttpBody());
        isHeatingInfoChangedLocally_m = false;
        lLastSynchroniseSuccessTime_m = millis();
        setUIState(UI_STATE_SYNCHRONISATION_COMPLETE, false);
    }
    else
    {
        setUIState(UI_STATE_SYNCHRONISATION_FAILED, false);
    }
    
    delay(1000);
    setUIState(UI_STATE_NORMAL, false);
    updateRelays();
    
}

